const std = @import("std");
const assert = std.debug.assert;
const log = std.log.scoped(.arp);
const allocator = std.heap.page_allocator;

const net = @import("../net.zig");
const Mac = net.Mac;
const Ether = net.ether.Ether;

/// an ARP packet (comes after an Ethernet header).
pub const Arp = extern struct {
    hw_type: Hw align(1) = .ether,
    proto_type: Ether.Type align(1) = .ip,
    hw_size: u8 align(1) = @sizeOf(Mac),
    pro_size: u8 align(1) = @sizeOf(u32),
    op: Op align(1),

    hw_src: Mac align(1) = net.local_mac,
    ip_src: u32 align(1) = net.local_ip,
    hw_dst: Mac align(1),
    ip_dst: u32 align(1),

    const Hw = enum(u16) {
        /// Ethernet
        ether = 1,
        _,
    };

    const Op = enum(u16) {
        /// requests hw addr given protocol addr
        request = 1,
        /// replies with the hw addr of the protocol addr
        reply = 2,

        _,
    };

    comptime {
        assert(@alignOf(@This()) == 1);
        assert(@bitSizeOf(@This()) / 8 == @sizeOf(@This()));
    }

    pub const header_size = @sizeOf(@This());
    pub const packet_lenth = Ether.packet_lenth + header_size;
};

pub fn recv(packet: []u8, eth: *Ether) !void {
    const S = struct {
        var seen_arp: bool = false;
    };

    if (S.seen_arp) return;

    log.info("received an ARP packet", .{});
    S.seen_arp = true;

    const arp: *Arp = @ptrCast(packet[Ether.packet_lenth..]);
    net.swapAllFields(arp);

    try reply(eth.shost, arp.ip_src);
}

// send an ARP reply packet to tell qemu to map xv6's ip address to its ethernet address. this is the
// bare minimum needed to persuade qemu to send IP packets to xv6, the real ARP protocol is more complex.
pub fn reply(hw_dst: Mac, ip_dst: u32) !void {
    const packet = try allocator.alloc(u8, Arp.packet_lenth);
    const arp: *Arp = @ptrCast(packet[Ether.packet_lenth..]);
    arp.* = .{
        .op = .reply,
        .hw_dst = hw_dst,
        .ip_dst = ip_dst,
    };

    net.swapAllFields(arp);
    try net.ether.send(packet, .arp, hw_dst);
}
